/*
 * Copyright 2011 J. Auer, J. Andert, F.-J. Jendrusch, U. Celebi
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.fuberlin.replication.performance.pc;

import java.util.HashMap;
import java.util.Random;

import de.fuberlin.replication.Replication;
import de.fuberlin.replication.application.SynchronizableAbstract;
import de.fuberlin.replication.communication.Communication;
import de.fuberlin.replication.communication.wifi.java.net.JavaNetWiFiCommunication;
import de.fuberlin.replication.library.Metadata;
import de.fuberlin.replication.library.Subset;
import de.fuberlin.replication.library.Version;
import de.fuberlin.replication.logging.Logger;
import de.fuberlin.replication.logging.defaultjava.DefaultLogger;
import de.fuberlin.replication.storage.Storage;
import de.fuberlin.replication.test.library.MetadataDAODummy;
import de.fuberlin.replication.test.library.StorageDummy;
import de.fuberlin.replication.test.library.SubsetDAODummy;

/**
 * Class, for testing the performance of the Vector Sets algorithm, regardless of storage- and communication-overhead.
 */
public class PerformanceSuite implements Runnable {

	private static final int NBROBJECTS = 1000;
	private static final int ITERATIONS = 10;
	private static final boolean DEBUG = false;
	
	private int nbrSubsets;
	private double percentageKnownObjects;
	private Distribution distribution;
	
	/**
	 * Constructor.
	 * 
	 * @param nbrSubsets Number of Subsets to create
	 * @param percentageKnownObjects Percentage of objects, that should be known to both replicas
	 * @param distribution How the objects are distributed among the subsets
	 */
	public PerformanceSuite(int nbrSubsets, double percentageKnownObjects, Distribution distribution) {
		this.nbrSubsets = nbrSubsets;
		this.percentageKnownObjects = percentageKnownObjects;
		this.distribution = distribution;
	}

	private enum Distribution {
		UNIFORM, GAUSSIAN};
		
	private enum Role {
		CONNECTOR, LISTENER
	};

	/**
	 * Normally, synchronization is locked to the Synchronizable Class-object. To allow multiple Threads usage of the library at the same time, these Wrappers are needed. 
	 */
	public class Actor1 extends Actor {
		public Actor1(String address, String remoteHost, Role role) {
			super(address, remoteHost, role);
		}
	}
		
	/**
	 * See above.
	 */
	public class Actor2 extends Actor {
		public Actor2(String address, String remoteHost, Role role) {
			super(address, remoteHost, role);
		}
	}
	
	/**
	 * Represents a replica. It tries to synchronize with a given remote Actor, as soon as started.
	 */
	public class Actor extends SynchronizableAbstract implements Runnable {
		private String address;
		private JavaNetWiFiCommunication communication;
		private Logger logger;
		private StorageDummy storage;
		private Role role;
		private String remoteHost;
		private HashMap<String, Object> objects;
		
		/**
		 * Constructor.
		 * 
		 * @param address Name of this actor
		 * @param remoteHost IP-address of the remote Actor
		 * @param role Role.CONNECTOR or Role.LISTENER according to this Actors connection role
		 */
		public Actor(String address, String remoteHost, Role role) {
			this.address = address;
			this.remoteHost = remoteHost;
			this.role = role;
			communication = new JavaNetWiFiCommunication();
			
			if(DEBUG)
				logger = new DefaultLogger();
			else
				logger = new LoggerDummy();
			
			storage = new StorageDummy();
			storage.metadataDAO = new MetadataDAODummy();
			storage.subsetDAO = new SubsetDAODummy();
			objects = new HashMap<String, Object>();
		}

		@Override
		public String getAddress() {
			return address;
		}

		@Override
		public Communication getCommunication() {
			return communication;
		}

		@Override
		public Logger getLogger() {
			return logger;
		}
		
		@Override
		public Storage getStorage() {
			return storage;
		}

		@Override
		public Object getObject(String name) {
			return objects.get(name);
		}

		@Override
		public void putObject(String name, Object o) {
			objects.put(name, o);
		}
		
		@Override
		public void run() {
			try {
				if (role == Role.CONNECTOR)
					communication.connect(remoteHost, 0);
				else
					communication.listen(0);

				Replication.synchronize(this);

			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				communication.close();
			}
		}
		
		/**
		 * Print everything this Actor knows.
		 */
		public void list() {
			System.out.println("Actor: "+getAddress());
			Iterable<Subset> subsets = storage.getSubsetDAO().getAll();
			
			for(Subset set:subsets) {
				System.out.println(set.getId()+"  "+set.getKnowledge().toString());
				Iterable<Metadata> objects = storage.getMetadataDAO().getAll(set);
				
				for(Metadata md:objects)
					System.out.print(md.toString());
				
				System.out.println();
			}
			System.out.println();
		}
	}

	/**
	 * Create data for a Actor.
	 * 
	 * @param nbrSubsets Number of Subsets
	 * @param percentageKnownObjects Percentage of objects known to both Actors
	 * @param distribution How the objects are distributed among the Subsets
	 * @param actor A Actor
	 * @throws Exception Some Exception
	 */
	public void createData(int nbrSubsets, double percentageKnownObjects, Distribution distribution, Actor actor) throws Exception {
		int knownObjects = (int)(percentageKnownObjects*NBROBJECTS);
		int createdObject = 1;
		Random random = new Random();
		
		for (int i = 0; i < nbrSubsets; i++) {
			Subset subset = actor.getStorage().getSubsetDAO().create();
			subset.getKnowledge().add(new Version("Known", knownObjects));
			actor.getStorage().getSubsetDAO().save(subset);
		}
		
		Iterable<Subset> subsetList = actor.getStorage().getSubsetDAO().getAll();
		Subset[] subsets = new Subset[nbrSubsets];
		int i = 0;
		
		for (Subset s : subsetList) {
			subsets[i++] = s;
		}
		
		String content = "Automatic created content "+random.nextLong();

		while (createdObject <= NBROBJECTS) {
			if(DEBUG)
				System.out.println(".....Creating content #" + createdObject);

			long nextVersion;;
			String address;
				
			if(createdObject <= knownObjects) {
				nextVersion = createdObject;
				address = "Known";
			} else {
				nextVersion = random.nextInt(NBROBJECTS);
				address = "Unknown";
			}
			
			Object o = new String("Automatic content #" + createdObject + content);
			Metadata m = new Metadata(address + "_AN#" + createdObject, new Version(address, nextVersion));
			actor.putObject("Automatic content #" + createdObject, o);
			Subset targetSet;
			
			if(distribution == Distribution.GAUSSIAN) {
				int gaussian = (int)(random.nextGaussian()*nbrSubsets/10+nbrSubsets/2);
				
				if(gaussian < 0)
					gaussian = 0;
				else if(gaussian >= nbrSubsets)
					gaussian = nbrSubsets-1;
				
				targetSet = subsets[gaussian];
			}
			else
				targetSet = subsets[random.nextInt(nbrSubsets)];

			if(createdObject > knownObjects) {
				// Merge to target subsets knowledge vector
				boolean replicaFound = false;
				
				for (Version v : targetSet.getKnowledge())
					if (v.getReplica().equals(address)) {
						v.setVersion(nextVersion);
						replicaFound = true;
						break;
					}

				// Add an entry for this replica if formerly unknown
				if (!replicaFound)
					targetSet.getKnowledge().add(new Version(address, nextVersion));
				
				actor.getStorage().getSubsetDAO().save(targetSet);
			}
			
			actor.getStorage().getMetadataDAO().save(targetSet, m);
			++createdObject;
		}
	}

	/**
	 * Initialize Actors and their data and run a test.
	 * 
	 * @return Duration of the synchronization in ms
	 * @throws Exception Some Exception
	 */
	public long performTest() throws Exception {
		System.out.println("Initializing...");

		Actor1 actor1 = new Actor1("A1", "127.0.0.1", Role.LISTENER);
		Actor2 actor2 = new Actor2("A2", "127.0.0.1", Role.CONNECTOR);
		Thread t1 = new Thread(actor1);
		Thread t2 = new Thread(actor2);
		
		createData(nbrSubsets, percentageKnownObjects, distribution, actor1);
		createData(nbrSubsets, percentageKnownObjects, distribution, actor2);
		
		if(DEBUG) {
			System.out.println("\nDEBUG:");
			actor1.list();
			actor2.list();
		}
		
		System.out.println("Starting connection and measure...");

		long start = System.currentTimeMillis();
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		long time = System.currentTimeMillis() - start;

		if(DEBUG) {
			System.out.println("\nDEBUG:");
			actor1.list();
			actor2.list();
		}
		System.out.println("...run finished.");
		
		return time;
	}

	public static void main(String[] args) throws Exception {
		// Run a bunch of tests
		Distribution[] distributions = {Distribution.UNIFORM, Distribution.GAUSSIAN};
		int[] subsets = {2,10,20,30,40,1000};
		double[] known = {0,0.25,0.5,0.75};
		
		for(Distribution d : distributions)
			for (int i:subsets)
				for(double k:known) {
					System.out.println("\n==== NEW TEST: D:"+d+" SS: "+i+" K: "+k+" ====");
					Thread t = new Thread(new PerformanceSuite(i,k,d));
					t.start();
					t.join();
				}
		
		// Optional: Run a single test specified via a promt:
		/*BufferedReader lineOfText = new BufferedReader(new InputStreamReader(System.in));
		int nbrSubsets = 0;
		double percentageKnownObjects = -1;
		String distribution = null;

		System.out.println("==== Performance Suite ====");

		while (nbrSubsets == 0) {
			System.out.println("How many Subsets do you want to create?");
			nbrSubsets = Integer.valueOf(lineOfText.readLine());
			
			if (nbrSubsets == 0)
				System.out.println("Please insert int!");

		}

		while (percentageKnownObjects == -1) {
			System.out.println("How many objects shall be known to both replicas?");
			percentageKnownObjects = Double.valueOf(lineOfText.readLine());
			
			if (percentageKnownObjects == -1 || percentageKnownObjects > 1) {
				System.out.println("Please insert double <= 1!");
				percentageKnownObjects = -1;
			}
		}

		while (distribution == null) {
			System.out.println("How shall the objects be distributed (u/n)?");
			distribution = lineOfText.readLine();
			
			if (distribution == null || (!distribution.equalsIgnoreCase("u") && !distribution.equalsIgnoreCase("n"))) {
				System.out.println("Please insert 'u' or 'n'!");
				distribution = null;
			}
		}

		System.out.println("Performing Test.....");
		
		if(distribution.equalsIgnoreCase("u"))
			new Thread(new PerformanceSuite(nbrSubsets, percentageKnownObjects, Distribution.UNIFORM)).start();
		else
			new Thread(new PerformanceSuite(nbrSubsets, percentageKnownObjects, Distribution.GAUSSIAN)).start();*/
	}

	@Override
	public void run() {
		try {
			long time = 0;
			
			for(int i=0; i<ITERATIONS; ++i) {
				time += performTest();
				Thread.sleep(2000);
			}
			
			time = time/ITERATIONS;
			System.out.println("Test Done!");
			System.out.println("Time needed: " + time + "ms");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}